#![allow(dead_code)]
use std::io::Read;
use super::chunk;

pub struct Reader<R: Read> {
    r: R
}

impl<R: Read> Reader<R> {
    pub fn new(r: R) -> Self {
        Self { r }
    }

    pub fn read_byte(&mut self) -> u8 {
        let mut c = [0; 1];
        self.r.read(&mut c).unwrap();
        c[0]
    }

    pub fn read_bytes(&mut self, n: usize) -> Vec<u8> {
        let mut bytes = vec![0; n];
        self.r.read(&mut bytes).unwrap();
        bytes
    }

    pub fn read_u32(&mut self) -> u32 {
        let mut c = [0; 4];
        self.r.read(&mut c).unwrap();
        u32::from_le_bytes(c)
    }

    pub fn read_u64(&mut self) -> u64 {
        let mut c = [0; 8];
        self.r.read(&mut c).unwrap();
        u64::from_le_bytes(c)
    }

    pub fn read_lua_integer(&mut self) -> i64 {
        self.read_u64() as i64
    }

    pub fn read_lua_number(&mut self) -> f64 {
        f64::from_bits(self.read_u64())
    }

    pub fn read_string(&mut self) -> String {
        let mut size = self.read_byte() as usize;
        if size == 0 {
            return String::new()
        }

        if size == 0xff {
            size = self.read_u64() as usize;
        }
        let bytes = self.read_bytes(size - 1);
        String::from_utf8(bytes).unwrap()
    }

    pub fn check_header(&mut self) {
        assert_eq!(self.read_bytes(4), chunk::LUA_SIGNATURE, "not a precompiled chunk!");
        assert_eq!(self.read_byte(), chunk::LUAC_VERSION, "version mismatch!");
        assert_eq!(self.read_byte(), chunk::LUAC_FORMAT, "format mismatch!");
        assert_eq!(self.read_bytes(6), chunk::LUAC_DATA, "corrupted!");
        assert_eq!(self.read_byte(), chunk::CINT_SIZE, "int size mismatch!");
        assert_eq!(self.read_byte(), chunk::CSIZE_T_SIZE, "size_t size mismatch!");
        assert_eq!(self.read_byte(), chunk::INSTRUCTION_SIZE, "instruction size mismatch!");
        assert_eq!(self.read_byte(), chunk::LUA_INTEGER_SIZE, "lua_Integer size mismatch!");
        assert_eq!(self.read_byte(), chunk::LUA_NUMBER_SIZE, "lua_Number size mismatch!");
        assert_eq!(self.read_lua_integer(), chunk::LUAC_INT, "endianness mismatch!");
        assert_eq!(self.read_lua_number(), chunk::LUAC_NUM, "float format mismatch!");
    }

    pub fn read_vals<T, F>(&mut self, proc: F) -> Vec<T> where F: Fn(&mut Self) -> T {
        let n = self.read_u32() as usize;
        let mut v = Vec::with_capacity(n);
        for _ in 0..n {
            v.push(proc(self));
        }
        v
    }

    pub fn read_proto(&mut self, parent_source: String) -> chunk::Prototype {
        let source = {
           let src = self.read_string();
           if src.is_empty() {
               parent_source
           } else {
               src
           }
        };

        chunk::Prototype {
            source: source.clone(),
            line_defined: self.read_u32(),
            last_line_defined: self.read_u32(),
            num_params: self.read_byte(),
            is_vararg: self.read_byte() != 0,
            max_stack_size: self.read_byte(),
            code: self.read_vals(Self::read_u32),
            constants: self.read_vals(Self::read_constant),
            upvalues: self.read_vals(Self::read_upvalue),
            protos: self.read_vals(|r| r.read_proto(source.clone())),
            line_info: self.read_vals(Self::read_u32),
            loc_vars: self.read_vals(Self::read_loc_var),
            upvalue_names: self.read_vals(Self::read_string),
        }
    }

    pub fn read_constant(&mut self) -> chunk::Constant {
        use chunk::Constant;
        let tag = self.read_byte();
        match tag {
            chunk::TAG_NIL => Constant::Nil,
            chunk::TAG_BOOLEAN => Constant::Boolean(self.read_byte() != 0),
            chunk::TAG_INTEGER => Constant::Integer(self.read_lua_integer()),
            chunk::TAG_NUMBER => Constant::Number(self.read_lua_number()),
            chunk::TAG_SHORT_STR | chunk::TAG_LONE_STR => Constant::Str(self.read_string()),
            _ => panic!("corrupted!")
        }
    }

    pub fn read_upvalue(&mut self) -> chunk::Upvalue {
        chunk::Upvalue {
            in_stack: self.read_byte(),
            idx: self.read_byte()
        }
    }

    pub fn read_loc_var(&mut self) -> chunk::LocVar {
        chunk::LocVar {
            var_name: self.read_string(),
            start_pc: self.read_u32(),
            end_pc: self.read_u32()
        }
    }
}